﻿using System;
using System.Collections.Generic;

namespace ContextualStateless
{
    /// <summary>
    /// Models behaviour as transitions between a finite set of states.
    /// </summary>
    /// <typeparam name="TState">The type used to represent the states.</typeparam>
    /// <typeparam name="TTrigger">The type used to represent the triggers that cause state transitions.</typeparam>
    /// <typeparam name="TContext">The type used to represent context</typeparam>
    public partial class StateMachine< TState, TTrigger, TContext >
    {
        #region Attributes

        /// <summary>
        /// Configured states.
        /// </summary>
        protected readonly IDictionary< TState, StateRepresentation > m_stateConfiguration = new Dictionary<TState, StateRepresentation>();

        /// <summary>
        /// Triggers with parameters.
        /// </summary>
        protected readonly IDictionary<TTrigger, TriggerWithParameters> m_triggerConfiguration = new Dictionary<TTrigger, TriggerWithParameters>();

        /// <summary>
        /// Delegate to state value retrieval method.
        /// </summary>
        protected readonly Func< TContext, TState > m_contextStateAccessor;

        /// <summary>
        /// Delegate to state value setting method.
        /// </summary>
        protected readonly Action< TState, TContext > m_contextStateMutator;

        /// <summary>
        /// Delegate to method, which is invoked when unhandled (not configured) trigger is fired.
        /// </summary>
        protected Action<TState, TTrigger> m_unhandledTriggerAction = DefaultUnhandledTriggerAction;

        #endregion

        #region Constructors

        /// <summary>
        /// Construct a state machine with external state storage.
        /// </summary>
        /// <param name="stateAccessor">A function that will be called to read the current state value.</param>
        /// <param name="stateMutator">An action that will be called to write new state values.</param>
        public StateMachine(Func<TState> stateAccessor, Action<TState> stateMutator)
        {
            Enforce.ArgumentNotNull( stateAccessor, "stateAccessor" );
            Enforce.ArgumentNotNull( stateMutator, "stateMutator" );
//            Contract.Requires( stateAccessor != null, "stateAccessor" );
//            Contract.Requires( stateMutator != null, "stateMutator" );

            m_contextStateAccessor = context => stateAccessor();
            m_contextStateMutator = ( state, context ) => stateMutator( state );
        }

        ///<summary>
        /// Constructs a state machine with external state storage.
        /// State acquisition and mutating depends on passed context
        ///</summary>
        ///<param name="contextStateAccessor">reference to context based state accessing method</param>
        ///<param name="contextStateMutator">reference to context based state mutating method</param>
        public StateMachine( Func< TContext, TState > contextStateAccessor, Action< TState, TContext > contextStateMutator )
        {
            m_contextStateAccessor = Enforce.ArgumentNotNull( contextStateAccessor, "contextStateAccessor" );
            m_contextStateMutator = Enforce.ArgumentNotNull( contextStateMutator, "contextStateMutator" );
//            Contract.Requires( contextStateAccessor != null, "contextStateAccessor" );
//            Contract.Requires( contextStateMutator != null, "contextStateMutator" );
//
//            m_contextStateAccessor = contextStateAccessor;
//            m_contextStateMutator = contextStateMutator;
        }

        /// <summary>
        /// Construct a state machine.
        /// </summary>
        /// <param name="initialState">The initial state.</param>
        public StateMachine( TState initialState )
        {
            var _stateReference = new StateReference { State = initialState };

            m_contextStateAccessor = context => _stateReference.State;
            m_contextStateMutator = ( state, context ) => _stateReference.State = state;
        }

        #endregion

        // --------------------------------------------------------------------------------------------------------------------

        /// <summary>
        /// Returns context based current state.
        /// </summary>
        /// <param name="context"></param>
        /// <returns></returns>
        public TState GetState( TContext context )
        {
            return m_contextStateAccessor( context );
        }

        /// <summary>
        /// Sets context based state.
        /// </summary>
        /// <param name="state"></param>
        /// <param name="context"></param>
        public void SetState( TState state, TContext context )
        {
            m_contextStateMutator( state, context );
        }

        /// <summary>
        /// The currently-permissible trigger values.
        /// </summary>
        public IEnumerable<TTrigger> PermittedTriggers( TContext context )
        {
            return CurrentStateRepresentation( context ).PermittedTriggers( context );
        }

        protected StateRepresentation CurrentStateRepresentation( TContext context )
        {
            return CurrentStateRepresentation( GetState( context ) );
        }

        protected StateRepresentation CurrentStateRepresentation( TState state )
        {
            return GetRepresentation( state );
        }

        protected StateRepresentation GetRepresentation(TState state)
        {
            StateRepresentation _stateRepresentation;

            if (!m_stateConfiguration.TryGetValue(state, out _stateRepresentation))
            {
                _stateRepresentation = new StateRepresentation(state);
                m_stateConfiguration.Add( state, _stateRepresentation);
            }

            return _stateRepresentation;
        }

        /// <summary>
        /// Begin configuration of the entry/exit actions and allowed transitions
        /// when the state machine is in a particular state.
        /// </summary>
        /// <param name="state">The state to configure.</param>
        /// <returns>A configuration object through which the state can be configured.</returns>
        public StateConfiguration Configure(TState state)
        {
            return new StateConfiguration( GetRepresentation( state ), GetRepresentation );
        }

        /// <summary>
        /// Transition from the current state via the specified trigger.
        /// The target state is determined by the configuration of the current state.
        /// Actions associated with leaving the current state and entering the new one
        /// will be invoked.
        /// </summary>
        /// <param name="trigger">The trigger to fire.</param>
        /// <param name="context">The context</param>
        /// <exception cref="System.InvalidOperationException">The current state does
        /// not allow the trigger to be fired.</exception>
        public void Fire( TTrigger trigger, TContext context )
        {
            InternalFire( trigger, context, new object[0] );
        }

        /// <summary>
        /// Transition from the current state via the specified trigger.
        /// The target state is determined by the configuration of the current state.
        /// Actions associated with leaving the current state and entering the new one
        /// will be invoked.
        /// </summary>
        /// <typeparam name="TArg0">Type of the first trigger argument.</typeparam>
        /// <param name="trigger">The trigger to fire.</param>
        /// <param name="context">The context</param>
        /// <param name="arg0">The first argument.</param>
        /// <exception cref="System.InvalidOperationException">The current state does
        /// not allow the trigger to be fired.</exception>
        public void Fire< TArg0 >( TriggerWithParameters<TArg0> trigger, TContext context, TArg0 arg0 )
        {
            Enforce.ArgumentNotNull(trigger, "trigger");
            //Contract.Requires( trigger != null, "trigger" );

            InternalFire(trigger.Trigger, context, arg0);
        }

        /// <summary>
        /// Transition from the current state via the specified trigger.
        /// The target state is determined by the configuration of the current state.
        /// Actions associated with leaving the current state and entering the new one
        /// will be invoked.
        /// </summary>
        /// <typeparam name="TArg0">Type of the first trigger argument.</typeparam>
        /// <typeparam name="TArg1">Type of the second trigger argument.</typeparam>
        /// <param name="context">The context</param>
        /// <param name="arg0">The first argument.</param>
        /// <param name="arg1">The second argument.</param>
        /// <param name="trigger">The trigger to fire.</param>
        /// <exception cref="System.InvalidOperationException">The current state does
        /// not allow the trigger to be fired.</exception>
        public void Fire<TArg0, TArg1>( TriggerWithParameters<TArg0, TArg1> trigger, TContext context, TArg0 arg0, TArg1 arg1 )
        {
            Enforce.ArgumentNotNull(trigger, "trigger");
            //Contract.Requires( trigger != null, "trigger" );

            InternalFire( trigger.Trigger, context, arg0, arg1 );
        }

        /// <summary>
        /// Transition from the current state via the specified trigger.
        /// The target state is determined by the configuration of the current state.
        /// Actions associated with leaving the current state and entering the new one
        /// will be invoked.
        /// </summary>
        /// <typeparam name="TArg0">Type of the first trigger argument.</typeparam>
        /// <typeparam name="TArg1">Type of the second trigger argument.</typeparam>
        /// <typeparam name="TArg2">Type of the third trigger argument.</typeparam>
        /// <param name="context">The context</param>
        /// <param name="arg0">The first argument.</param>
        /// <param name="arg1">The second argument.</param>
        /// <param name="arg2">The third argument.</param>
        /// <param name="trigger">The trigger to fire.</param>
        /// <exception cref="System.InvalidOperationException">The current state does
        /// not allow the trigger to be fired.</exception>
        public void Fire<TArg0, TArg1, TArg2>( TriggerWithParameters<TArg0, TArg1, TArg2> trigger, TContext context, TArg0 arg0, TArg1 arg1, TArg2 arg2)
        {
            Enforce.ArgumentNotNull(trigger, "trigger");
            //Contract.Requires( trigger != null, "trigger" );

            InternalFire( trigger.Trigger, context, arg0, arg1, arg2);
        }
        
        protected internal void InternalFire( TTrigger trigger, TContext context, params object[] args )
        {
            var _currentState = GetState( context );
            var _currentStateRepresentation = CurrentStateRepresentation( _currentState );

            AbstractTriggerRepresentation _trigger;

            if ( !_currentStateRepresentation.TryFindHandler( trigger, context, out _trigger ) )
            {
                m_unhandledTriggerAction( _currentStateRepresentation.UnderlyingState, trigger);
                return;
            }

            TriggerWithParameters _parametrizedTrigger;

            if ( m_triggerConfiguration.TryGetValue( trigger, out _parametrizedTrigger ) )
                _parametrizedTrigger.ValidateParameters( args );

            TState _destinationState;

            if ( !_trigger.ResultsInTransitionFrom( _currentState, args, context, out _destinationState ) )
                return;

            var _stateTransition = new StateTransition( _currentState, _destinationState, trigger );

            // executing handler
            if ( !_trigger.ContextHandler( context, trigger ) )
                return;

            _currentStateRepresentation.Exit( _stateTransition );
            SetState( _stateTransition.Destination, context );
            CurrentStateRepresentation( context ).Enter( _stateTransition, args );
        }

        /// <summary>
        /// Override the default behaviour of throwing an exception when an unhandled trigger
        /// is fired.
        /// </summary>
        /// <param name="unhandledTriggerAction">An action to call when an unhandled trigger is fired.</param>
        public void OnUnhandledTrigger(Action<TState, TTrigger> unhandledTriggerAction)
        {
            //Contract.Requires< ArgumentException >( unhandledTriggerAction != null, "unhandledTriggerAction" );

            if (unhandledTriggerAction == null) 
                throw new ArgumentNullException("unhandledTriggerAction");

            m_unhandledTriggerAction = unhandledTriggerAction;
        }

        /// <summary>
        /// Determine if the state machine is in the supplied state.
        /// </summary>
        /// <param name="state">The state to test for.</param>
        /// <param name="context">The context</param>
        /// <returns>True if the current state is equal to, or a substate of,
        /// the supplied state.</returns>
        public bool IsInState(TState state, TContext context )
        {
            return CurrentStateRepresentation( context ).IsIncludedIn(state);
        }

        /// <summary>
        /// Returns true if <paramref name="trigger"/> can be fired
        /// in the current state.
        /// </summary>
        /// <param name="trigger">Trigger to test.</param>
        /// <param name="context">The context</param>
        /// <returns>True if the trigger can be fired, false otherwise.</returns>
        public bool CanFire( TTrigger trigger, TContext context )
        {
            return CurrentStateRepresentation( context ).CanHandle( trigger, context );
        }

        /// <summary>
        /// Specify the arguments that must be supplied when a specific trigger is fired.
        /// </summary>
        /// <typeparam name="TArg0">Type of the first trigger argument.</typeparam>
        /// <param name="trigger">The underlying trigger value.</param>
        /// <returns>An object that can be passed to the Fire() method in order to 
        /// fire the parameterised trigger.</returns>
        public TriggerWithParameters<TArg0> SetTriggerParameters<TArg0>(TTrigger trigger)
        {
            var _parameterizedTrigger = new TriggerWithParameters<TArg0>(trigger);
            SaveTriggerConfiguration(_parameterizedTrigger);

            return _parameterizedTrigger;
        }

        /// <summary>
        /// Specify the arguments that must be supplied when a specific trigger is fired.
        /// </summary>
        /// <typeparam name="TArg0">Type of the first trigger argument.</typeparam>
        /// <typeparam name="TArg1">Type of the second trigger argument.</typeparam>
        /// <param name="trigger">The underlying trigger value.</param>
        /// <returns>An object that can be passed to the Fire() method in order to 
        /// fire the parameterised trigger.</returns>
        public TriggerWithParameters<TArg0, TArg1> SetTriggerParameters<TArg0, TArg1>(TTrigger trigger)
        {
            var _parameterizedTrigger = new TriggerWithParameters<TArg0, TArg1>(trigger);
            SaveTriggerConfiguration( _parameterizedTrigger );

            return _parameterizedTrigger;
        }

        /// <summary>
        /// Specify the arguments that must be supplied when a specific trigger is fired.
        /// </summary>
        /// <typeparam name="TArg0">Type of the first trigger argument.</typeparam>
        /// <typeparam name="TArg1">Type of the second trigger argument.</typeparam>
        /// <typeparam name="TArg2">Type of the third trigger argument.</typeparam>
        /// <param name="trigger">The underlying trigger value.</param>
        /// <returns>An object that can be passed to the Fire() method in order to 
        /// fire the parameterised trigger.</returns>
        public TriggerWithParameters<TArg0, TArg1, TArg2> SetTriggerParameters<TArg0, TArg1, TArg2>(TTrigger trigger)
        {
            var _parameterizedTrigger = new TriggerWithParameters<TArg0, TArg1, TArg2>(trigger);
            SaveTriggerConfiguration(_parameterizedTrigger);

            return _parameterizedTrigger;
        }

        protected void SaveTriggerConfiguration(TriggerWithParameters trigger)
        {
//            Contract.Requires( trigger != null, "trigger" );
//            Contract.Requires< InvalidOperationException>( !m_triggerConfiguration.ContainsKey( trigger.Trigger ), string.Format( StateMachineResources.CannotReconfigureParameters, trigger ) );

            if (m_triggerConfiguration.ContainsKey(trigger.Trigger))
                throw new InvalidOperationException( string.Format(StateMachineResources.CannotReconfigureParameters, trigger) );

            m_triggerConfiguration.Add(trigger.Trigger, trigger);
        }

        protected static void DefaultUnhandledTriggerAction(TState state, TTrigger trigger)
        {
            throw new InvalidOperationException( string.Format( StateMachineResources.NoTransitionsPermitted, trigger, state));
        }
    }
}
